# -*- coding: utf-8 -*-

"""
Code to support recording to a readable and executable Python script.


TODO:
    - Support for dictionaries?

Author: Prabhu Ramachandran <prabhu@aero.iitb.ac.in>
Copyright (c) 2008, Enthought, Inc.
License: BSD Style.
"""

__all__ = ['CustomRecorder']

import types
import __builtin__

from traits.util.camel_case import camel_case_to_python
from traits.api import List, Unicode, Bool

from apptools.scripting.api import Recorder, RecorderError
################################################################################
# `Recorder` class.
################################################################################
class CustomRecorder(Recorder):

    escape_chars = Bool(True)
    lines = List(Unicode)

    ######################################################################
    # `Recorder` interface.
    ######################################################################
    def record(self, code):
        """Record a string to be stored to the output file.

        Parameters:
        -----------

        code - A string of text.
        """
        if self.recording and not self._in_function:
            lines = self.lines
            # Analyze the code and add extra code if needed.
            self._analyze_code(code)
            # Add the code.
            lines.append(code)

    def record_function(self, func, args, kw):
        """Record a function call given the function and its
        arguments."""
        if self.recording and not self._in_function:
            # Record the function name and arguments.
            call_str = self._function_as_string(func, args, kw)
            # Call the function.
            try:
                self._in_function = True
                result = func(*args, **kw)
            finally:
                self._in_function = False

            # Register the result if it is not None.
            if func.__name__ == '__init__':
                f_self = args[0]
                code = self._import_class_string(f_self.__class__)
                self.lines.append(code)
                return_str = self._registry.get(f_self).script_id
            else:
                return_str = self._return_as_string(result)
            if len(return_str) > 0:
                self.lines.append('%s = %s' % (return_str, call_str))
            else:
                self.lines.append('%s' % (call_str))
        else:
            result = func(*args, **kw)
        return result

    ######################################################################
    # Non-public interface.
    ######################################################################
    def _get_unique_name(self, obj):
        """Return a unique object name (a string).  Note that this does
        not cache the object, so if called with the same object 3 times
        you'll get three different names.
        """
        cname = obj.__class__.__name__
        nm = self._name_map
        result = ''
        builtin = False
        if cname in __builtin__.__dict__:
            builtin = True
            if hasattr(obj, '__name__'):
                cname = obj.__name__
        else:
            cname = camel_case_to_python(cname)

        special_ids = self._special_ids
        while len(result) == 0 or result in special_ids:
            if cname in nm:
                id = nm[cname] + 1
                nm[cname] = id
                result = '%s%d' % (cname, id)
            else:
                nm[cname] = 0
                # The first id doesn't need a number if it isn't builtin.
                if builtin:
                    result = '%s0' % (cname)
                else:
                    result = cname
        return result

    def _get_registry_data(self, object):
        """Get the data for an object from registry."""
        data = self._registry.get(object)
        if data is None:
            msg = "Recorder: Can't get script_id since object %s not registered"
            raise RecorderError(msg % (object))
        return data

    def _listner(self, object, name, old, new):
        """The listner for trait changes on an object.

        This is called by child listners or when any of the recordable
        object's traits change when recording to a script is enabled.

        Parameters:
        -----------

        object : Object which has changed.

        name : extended name of attribute that changed.

        old : Old value.

        new : New value.

        """
        if self.recording and not self._in_function:
            new_repr = repr(new)
            if self.escape_chars :
              pass
            else :
              new_unicode = self._object_as_string(new)
            sid = self._get_registry_data(object).script_id
            if len(sid) == 0:
                msg = u'%s = %s' % (name, new)
            else:
                msg = u'%s.%s = %s' % (sid, name, new)
            if new_repr.startswith('<') and new_repr.endswith('>'):
                self.record(u'# ' + msg)
            else:
                self.record(msg)

    def _list_items_listner(self, object, name, old, event):
        """The listner for *_items on list traits of the object.
        """
        # Set the path of registered objects in the modified list and
        # all their children.  This is done by unregistering the object
        # and re-registering them.  This is slow but.
        registry = self._registry
        sid = registry.get(object).script_id
        trait_name = name[:-6]
        items = getattr(object, trait_name)
        for (i, item) in enumerate(items):
            if item in registry:
                data = registry.get(item)
                tnop = data.trait_name_on_parent
                if len(tnop) > 0:
                    data.trait_name_on_parent = '%s[%d]' % (trait_name, i)

        # Record the change.
        if self.recording and not self._in_function:
            index = event.index
            removed = event.removed
            added = event.added
            nr = len(removed)
            slice = '[%d:%d]' % (index, index + nr)
            na = len(added)
            rhs = [self._object_as_string(item) for item in added]
            rhs = ', '.join(rhs)
            obj = '%s.%s' % (sid, name[:-6])
            msg = '%s%s = [%s]' % (obj, slice, rhs)
            self.record(msg)

    def _object_changed_handler(self, object, name, old, new):
        """Called when a child recordable object has been reassigned."""
        registry = self._registry
        if old is not None:
            if old in registry:
                self.unregister(old)
        if new is not None:
            if new not in registry:
                self.register(new, parent=object,
                              trait_name_on_parent=name)

    def _analyze_code(self, code):
        """Analyze the code and return extra code if needed.
        """
        known_ids = self._known_ids
        lhs = ''
        try:
            lhs = code.split()[0]
        except IndexError:
            pass

        if '.' in lhs:
            ob_name = lhs.split('.')[0]
            self.write_script_id_in_namespace(ob_name)

    def _function_as_string(self, func, args, kw):
        """Return a string representing the function call."""
        func_name = func.__name__
        func_code = func.func_code
        # Even if func is really a decorated method it never shows up as
        # a bound or unbound method here, so we have to inspect the
        # argument names to figure out if this is a method or function.
        if func_code.co_argcount > 0 and \
           func_code.co_varnames[0] == 'self':
            # This is a method, the first argument is bound to self.
            f_self = args[0]
            # Convert the remaining arguments to strings.
            argl = [self._object_as_string(arg) for arg in args[1:]]

            # If this is __init__ we special case it.
            if func_name == '__init__':
                # Register the object.
                self.register(f_self, known=True)
                func_name = f_self.__class__.__name__
            else:
                sid = self._object_as_string(f_self)
                func_name = '%s.%s' % (sid, func_name)
        else:
            argl = [self._object_as_string(arg) for arg in args]

        # Convert the keyword args.
        kwl = ['%s=%s' % (key, self._object_as_string(value))
               for key, value in kw.iteritems()]
        argl.extend(kwl)

        # Make a string representation of the args, kw.
        argstr = ', '.join(argl)
        return '%s(%s)' % (func_name, argstr)

    def _object_as_string(self, object):
        """Return a string representing the object.
        """
        registry = self._registry
        if object in registry:
            # Return script id if the object is known; create the script
            # id on the namespace if needed before that.
            sid = registry.get(object).script_id
            base_id = sid.split('.')[0]
            self.write_script_id_in_namespace(base_id)
            return sid
        else:
            # Try and return the object.
            ob_id = id(object)
            if isinstance(object, unicode) and not self.escape_chars:
              orepr = object.replace(u"'", u"\\'")
              orepr = u"u'%s'" % orepr
            else :
              orepr = repr(object)
            # As done in appscripting, we assume that if the hexid of
            # the object is in its string representation then it is an
            # arbitrary object. We remove the leading '0x' so that leading
            # zeros after the 'x' in the orepr string will not lead to a
            # mismatch. (ie. 0xA3E will not match 0x0A3E)
            if hex(ob_id)[2:].upper() not in orepr.upper():
                return orepr

        # If we get here, we just register the object and call ourselves
        # again to do the needful.
        self.register(object)
        return self._object_as_string(object)

    def _return_as_string(self, object):
        """Return a string given a returned object from a function.
        """
        result = ''
        ignore = (types.FloatType, types.ComplexType, types.BooleanType,
                  types.IntType, types.LongType) + types.StringTypes
        if object is not None and type(object) not in ignore:
            # If object is not know, register it.
            registry = self._registry
            if object not in registry:
                self.register(object)
            result = registry.get(object).script_id
            # Since this is returned it is known on the namespace.
            known_ids = self._known_ids
            if result not in known_ids:
                known_ids.append(result)
        return result

    def _import_class_string(self, cls):
        """Import a class if needed.
        """
        cname = cls.__name__
        result = ''
        if cname not in __builtin__.__dict__:
            mod = cls.__module__
            typename = '%s.%s' % (mod, cname)
            if typename not in self._known_types:
                result = 'from %s import %s' % (mod, cname)
                self._known_types.append(typename)
        return result

